﻿using System;
using System.Collections.Generic;
using System.IO;
using WgFrame.Net.Protocols;
using WgFrame.Net.Socket;
using WgFrame.Security;

namespace WgFrame.Net.Peer
{
    public delegate void MyAction();

    public abstract class PeerBase
    {
        private static readonly byte[] tcpMsgHead = new byte[] { 243, 2 };

        protected IProtocol SerializationProtocol;

        protected static MessageBufferPool MessageBufferPool = new MessageBufferPool();

        protected readonly Queue<MyAction> ActionQueue = new Queue<MyAction>();

        protected ICryptoProvider CryptoProvider;

        protected IWgSocket wgSocket;

        internal WgPeer wgPeer;

        internal ConnectionProtocol usedTransportProtocol;

        internal bool ApplicationIsInitialized;

        internal ConnectionStateValue peerConnectionState;

        internal long timeBase;

        internal long lastPingResult;

        internal bool isEncryptionAvailable;

        internal long serverTimeOffset;

        internal bool serverTimeOffsetIsAvailable;

        internal long timestampOfLastReceive;

        internal long roundTripTime;

        internal long roundTripTimeVariance;

        internal long lastRoundTripTime;

        internal long lowestRoundTripTime;

        internal long highestRoundTripTimeVariance;

        internal object CustomInitData;

        internal long timeInt { get { return SupportClass.GetTickCount() - timeBase; } }

        internal IWgPeerListener Listener { get { return wgPeer.Listener; } }

        internal DebugLevel debugOut { get { return wgPeer.DebugOut; } }

        internal int DisconnectTimeout { get { return wgPeer.DisconnectTimeout; } }

        internal int timePingInterval { get { return wgPeer.TimePingInterval; } }

        public string ServerAddress { get; internal set; }

        public string AppId { get; internal set; }

        public string ConnectionId { get; protected set; }

        internal abstract bool Connect(string serverAddress, string appID);

        internal virtual void OnConnect()
        {
            lastPingResult = SupportClass.GetTickCount();
        }

        internal abstract void Disconnect();

        protected abstract void StopConnection();

        internal void EnqueueDebugReturn(DebugLevel level, string debugReturn)
        {
            lock (ActionQueue)
            {
                ActionQueue.Enqueue(delegate
                {
                    Listener.DebugReturn(level, debugReturn);
                });
            }
        }

        internal void EnqueueStatusCallback(StatusCode statusValue)
        {
            lock (ActionQueue)
            {
                ActionQueue.Enqueue(delegate
                {
                    Listener.OnStatusChanged(statusValue);
                });
            }
        }

        internal void EnqueueActionForDispatch(MyAction action)
        {
            lock (ActionQueue)
            {
                ActionQueue.Enqueue(action);
            }
        }

        internal bool ExchangeKeysForEncryption(object lockObject)
        {
            isEncryptionAvailable = false;
            if (CryptoProvider != null)
            {
                CryptoProvider.Dispose();
                CryptoProvider = null;
            }
            CryptoProvider = new DiffieHellmanCryptoProvider();
            Dictionary<byte, object> parameters = new Dictionary<byte, object>(1);
            parameters[WgCodes.ClientKey] = CryptoProvider.PublicKey;
            SendOptions sendOptions;
            if (lockObject != null)
            {
                lock (lockObject)
                {
                    sendOptions = new SendOptions();
                    sendOptions.Encrypt = false;
                    return EnqueueOperation(parameters, WgCodes.InitEncryption, sendOptions, EgMessageType.InternalOperationRequest);
                }
            }
            sendOptions = new SendOptions();
            sendOptions.Encrypt = false;
            return EnqueueOperation(parameters, WgCodes.InitEncryption, sendOptions, EgMessageType.InternalOperationRequest);
        }

        internal abstract bool SendOutgoingCommands();

        internal abstract bool DispatchIncomingCommands();

        internal abstract void ReceiveIncomingCommands(byte[] inBuff, int len);

        internal abstract bool EnqueueOperation(Dictionary<byte, object> parameters, short opCode, SendOptions sendParams, EgMessageType messageType = EgMessageType.Operation);

        internal virtual StreamBuffer SerializeOperationToMessage(short opCode, Dictionary<byte, object> parameters, EgMessageType messageType, bool encrypt)
        {
            bool payloadEncrypt = encrypt && usedTransportProtocol != ConnectionProtocol.WebSocketSecure && usedTransportProtocol != ConnectionProtocol.HttpSecure;
            StreamBuffer serializeMemStream = MessageBufferPool.Get();
            serializeMemStream.SetLength(0);
            if (!payloadEncrypt)
            {
                serializeMemStream.Write(tcpMsgHead, 0, tcpMsgHead.Length);
            }
            SerializationProtocol.SerializeOperationRequest(serializeMemStream, opCode, parameters, false);
            if (payloadEncrypt)
            {
                byte[] encryptedBytes = CryptoProvider.Encrypt(serializeMemStream.GetBuffer(), 0, serializeMemStream.Length);
                serializeMemStream.SetLength(0);
                serializeMemStream.Write(tcpMsgHead, 0, tcpMsgHead.Length);
                serializeMemStream.Write(encryptedBytes, 0, encryptedBytes.Length);
            }
            byte[] fullMessageBytes = serializeMemStream.GetBuffer();
            if (messageType != EgMessageType.Operation)
            {
                fullMessageBytes[tcpMsgHead.Length - 1] = (byte)messageType;
            }
            if (payloadEncrypt)
            {
                fullMessageBytes[tcpMsgHead.Length - 1] = (byte)(fullMessageBytes[tcpMsgHead.Length - 1] | 0x80u);
            }
            return serializeMemStream;
        }

        protected abstract void FetchServerTimestamp();

        protected virtual bool DeserializeMessageAndCallback(StreamBuffer stream)
		{
			if (stream.Length < 2)
            {
                Listener.DebugReturn(DebugLevel.ERROR, "Incoming data too short! " + stream.Length);
                return false;
			}
			byte magicByte = stream.ReadByte();
			if (magicByte != 243)
			{
                Listener.DebugReturn(DebugLevel.ALL, "No regular operation message: " + magicByte);
                return false;
			}
			byte msgByte = stream.ReadByte();
			byte msgType = (byte)(msgByte & 0x7Fu);
			bool isEncrypted = (msgByte & 0x80) > 0;
			if (msgType != 1)
			{
				try
				{
					if (isEncrypted)
					{
						byte[] decryptedBuff = CryptoProvider.Decrypt(stream.GetBuffer(), 2, stream.Length - 2);
						stream = new StreamBuffer(decryptedBuff);
					}
					else
					{
						stream.Seek(2L, SeekOrigin.Begin);
					}
				}
				catch (Exception ex)
				{
					if ((int)debugOut >= 1)
					{
						Listener.DebugReturn(DebugLevel.ERROR, "msgType: " + msgType + " exception: " + ex.ToString());
					}
					SupportClass.WriteStackTrace(ex);
					return false;
				}
			}
			switch ((EgMessageType)msgType)
			{
				case EgMessageType.InitResponse:
                    {
                        OperationResponse opRes;
                        try
                        {
                            opRes = SerializationProtocol.DeserializeOperationResponse(stream);
                            InitCallback(opRes);
                        }
                        catch (Exception e)
                        {
                            EnqueueDebugReturn(DebugLevel.ERROR, "Deserialization failed for InitResponse. " + e);
                            return false;
                        }
                        break;
                    }
				case EgMessageType.InternalOperationResponse:
					{
						OperationResponse opRes;
						try
						{
							opRes = SerializationProtocol.DeserializeOperationResponse(stream);
						}
						catch (Exception e)
						{
							EnqueueDebugReturn(DebugLevel.ERROR, "Deserialization failed for internal Operation Response. " + e);
							return false;
						}
						if (opRes.OperationCode == WgCodes.InitEncryption)
						{
							DeriveSharedKey(opRes);
						}
						else if (opRes.OperationCode == WgCodes.Ping)
						{
                            ReadPingResult(opRes);
                        }
						else
						{
							EnqueueDebugReturn(DebugLevel.ERROR, "Received unknown internal operation. " + opRes.ToStringFull());
						}
						break;
                    }
                case EgMessageType.OperationResponse:
                    {
                        OperationResponse opRes;
                        try
                        {
                            opRes = SerializationProtocol.DeserializeOperationResponse(stream);
                        }
                        catch (Exception e)
                        {
                            EnqueueDebugReturn(DebugLevel.ERROR, "Deserialization failed for Operation Response. " + e);
                            return false;
                        }
                        Listener.OnOperationResponse(opRes);
                        break;
                    }
                default:
					EnqueueDebugReturn(DebugLevel.ERROR, "unexpected msgType " + msgType);
					break;
			}
			return true;
        }

        protected virtual void InitPeerBase()
        {
            SerializationProtocol = new Protocol16();
            peerConnectionState = ConnectionStateValue.Disconnected;
            timeBase = SupportClass.GetTickCount();
            isEncryptionAvailable = false;
            ApplicationIsInitialized = false;
            timestampOfLastReceive = SupportClass.GetTickCount();
        }

        protected void SendData(byte[] data, int length)
        {
            try
            {
                wgSocket.Send(data, length);
            }
            catch (Exception ex)
            {
                if ((int)debugOut >= 1)
                {
                    Listener.DebugReturn(DebugLevel.ERROR, ex.ToString());
                }
                SupportClass.WriteStackTrace(ex);
            }
        }

        private void InitCallback(OperationResponse response)
        {
            if (peerConnectionState == ConnectionStateValue.Connecting)
            {
                peerConnectionState = ConnectionStateValue.Connected;
            }
            ConnectionId = (string)response[1];
            ApplicationIsInitialized = true;
            FetchServerTimestamp();
            Listener.OnStatusChanged(StatusCode.Connect);
        }

        private void DeriveSharedKey(OperationResponse operationResponse)
        {
            if (operationResponse.ReturnCode != 0)
            {
                EnqueueDebugReturn(DebugLevel.ERROR, "Establishing encryption keys failed. " + operationResponse.ToStringFull());
                EnqueueStatusCallback(StatusCode.EncryptionFailedToEstablish);
                return;
            }
            byte[] serverPublicKey = (byte[])operationResponse[WgCodes.ServerKey];
            if (serverPublicKey == null || serverPublicKey.Length == 0)
            {
                EnqueueDebugReturn(DebugLevel.ERROR, "Establishing encryption keys failed. Server's public key is null or empty. " + operationResponse.ToStringFull());
                EnqueueStatusCallback(StatusCode.EncryptionFailedToEstablish);
            }
            else
            {
                CryptoProvider.DeriveSharedKey(serverPublicKey);
                isEncryptionAvailable = true;
                EnqueueStatusCallback(StatusCode.EncryptionEstablished);
                string info = "";
                for (int i=0; i<serverPublicKey.Length; i++)
                {
                    info += serverPublicKey[i].ToString() + ",";
                }
                EnqueueDebugReturn(DebugLevel.INFO, "Establishing encryption keys succ. public key is " + info);
            }
        }

        protected void SendPing()
        {
            long currentTimestamp = (lastPingResult = SupportClass.GetTickCount());
            SendOptions sendParams = default;
            StreamBuffer streamBuffer = SerializeOperationToMessage(WgCodes.Ping, new Dictionary<byte, object>
                {
                    {
                        1,
                        currentTimestamp
                    }
                }, EgMessageType.InternalOperationRequest, sendParams.Encrypt);


            byte[] data = streamBuffer.ToArray();
            SendData(data, data.Length);
            MessageBufferPool.Put(streamBuffer);
        }

        private void ReadPingResult(OperationResponse operationResponse)
        {
            long serverSentTime = (long)operationResponse.Parameters[2];
            long clientSentTime = (long)operationResponse.Parameters[1];
            lastRoundTripTime = SupportClass.GetTickCount() - clientSentTime;
            if (!serverTimeOffsetIsAvailable)
            {
                roundTripTime = lastRoundTripTime;
            }
            UpdateRoundTripTimeAndVariance(lastRoundTripTime);
            if (!serverTimeOffsetIsAvailable)
            {
                serverTimeOffset = serverSentTime + (lastRoundTripTime >> 1) - SupportClass.GetTickCount();
                serverTimeOffsetIsAvailable = true;
            }
        }

        private void UpdateRoundTripTimeAndVariance(long lastRoundtripTime)
        {
            if (lastRoundtripTime >= 0)
            {
                roundTripTimeVariance -= roundTripTimeVariance / 4;
                if (lastRoundtripTime >= roundTripTime)
                {
                    roundTripTime += (lastRoundtripTime - roundTripTime) / 8;
                    roundTripTimeVariance += (lastRoundtripTime - roundTripTime) / 4;
                }
                else
                {
                    roundTripTime += (lastRoundtripTime - roundTripTime) / 8;
                    roundTripTimeVariance -= (lastRoundtripTime - roundTripTime) / 4;
                }
                if (roundTripTime < lowestRoundTripTime)
                {
                    lowestRoundTripTime = roundTripTime;
                }
                if (roundTripTimeVariance > highestRoundTripTimeVariance)
                {
                    highestRoundTripTimeVariance = roundTripTimeVariance;
                }
            }
        }
    }
}
